import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Rectangle2D;
import java.io.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import javax.swing.JFileChooser;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.filechooser.FileNameExtensionFilter;

/** ClickyPanel Class :D
    CS158A Spring 2010 Project #2
    ClickyPanel: a place for the end-user to set up a LAN.
    @author Clare Lanam and Loc Phan
    */

public class ClickyPanel extends JPanel
{
   /** currently selected bridge #1 */
   private BridgeIcon selectedA;
   /** currently selected bridge #2 */
   private BridgeIcon selectedB;
   /** bridges displayed on this panel */
   private ArrayList<BridgeIcon> bridges;
   /** threads running the bridges */
   private ArrayList<Thread> threads;
   /** links existent on this panel */
   private LinkedList<LinkGraphic> links;
   /** listeners attached to this panel */
   private ArrayList<ChangeListener> listeners;
   /** link cost for new links */
   private int linkCost;
   /** how many bridges have been created total? -- for uniqueness */
   private int numBridgesMade;
   /** is the simulation currently running? */
   private boolean isRunning;
   
   // ------[FOR SAVING/LOADING]------ //
   
   /** file chooser -- open & save dialogs */
   private JFileChooser fileChooser;
   /** ID string to tell whether saving a bridge icon */
   private static final String IS_BRIDGE = "brIdgEIcOn_snoopy";
   /** ID string to tell whether saving a link graphic */
   private static final String IS_LINK = "lInkgrAphIc_woodstock";
   
   // ------[STATICS]------ //
   
   /** width of the panel */
   public static final int PANEL_WIDTH = 800;
   /** height of the panel */
   public static final int PANEL_HEIGHT = 400;
   /** left/right margin space -- don't draw too close to edge*/
   private static final int BUFFER_X = 30;
   /** top/bottom margin space -- don't draw too close to edge*/
   private static final int BUFFER_Y = 20;
   /** panel border thickness */
   private static final int BORDER = 2;
   /** background rectangle */
   private static final Rectangle RECT_BG
      = new Rectangle(0,0,PANEL_WIDTH, PANEL_HEIGHT);
   /** foreground/inner rectangle */
   private static final Rectangle RECT_FG = new Rectangle(
         BORDER,BORDER,PANEL_WIDTH-2*BORDER, PANEL_HEIGHT-2*BORDER);
   
   // ------[FUN STUFF]------ //
   
   /** Bridges that have been deleted. */
   private LinkedList<BridgeIcon> deletedBridges;
   /** Links that have been deleted. */
   private LinkedList<LinkGraphic> deletedLinks;
   
   /** serial ID added by Eclipse compiler */
   private static final long serialVersionUID = 1L;
   
   // ------[CONSTRUCTOR]------ //
   
   /** Initializes variables, sets up panel, adds mouse
       interactivity, and sets up repaint triggered by timer.
       */
   
   public ClickyPanel()
   {
      setPreferredSize(new Dimension(PANEL_WIDTH,PANEL_HEIGHT));
      setSize(new Dimension(PANEL_WIDTH,PANEL_HEIGHT));
      
      bridges = new ArrayList<BridgeIcon>();
      threads = new ArrayList<Thread>();
      listeners = new ArrayList<ChangeListener>();
      links = new LinkedList<LinkGraphic>();
      
      numBridgesMade = 0;
      linkCost = 1; // default
      isRunning = false;
      
      deletedBridges = new LinkedList<BridgeIcon>();
      deletedLinks = new LinkedList<LinkGraphic>();
      
      // ------:file chooser initialization:------
      
      fileChooser = new JFileChooser();
      fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
      fileChooser.setFileFilter(new FileNameExtensionFilter(
        "STP files", "stp"));
      
      // ------:mouse interactivity:------
      
      addMouseListener(new MouseListener()
      {
         public void mouseClicked(MouseEvent e) { ; }
         public void mouseEntered(MouseEvent e) { ; }
         public void mouseExited(MouseEvent e) { ; }
         public void mouseReleased(MouseEvent e) { ; }
         
         public void mousePressed(MouseEvent e)
         {
            processClick(e.getX(), e.getY());
            triggerChangeEvent();
         }
      });
      
      // ------:action listener for the timer:------
      
      ActionListener alTimer = new ActionListener()
      {
         private final int FADE_INC = -50;
         
         public void actionPerformed(ActionEvent event)
         {
            Iterator<BridgeIcon> iterBI = deletedBridges.iterator();
            while (iterBI.hasNext())
            {
               BridgeIcon b = iterBI.next();
               b.incrementAlpha(FADE_INC);
               if (b.getAlpha() <= 0)
                  iterBI.remove();
            }
            
            Iterator<LinkGraphic> iterLG = deletedLinks.iterator();
            while (iterLG.hasNext())
            {
               LinkGraphic lg = iterLG.next();
               lg.incrementAlpha(FADE_INC);
               if (lg.getAlpha() <= 0)
                  iterLG.remove();
            }
            
            repaint();
         }
      };

      // ------:update panel every DELAY milliseconds:------
      
      final int DELAY = 100; // 10 fps
      Timer t = new Timer(DELAY, alTimer);
      t.start();
   }
   
   // ------[PAINT METHOD]------ //
   
   /** Paints this panel.
       @param g the graphics context
       */
   
   public void paint(Graphics g)
   {
      Graphics2D g2 = (Graphics2D)g;
      
      g2.setColor(Color.BLACK);
      g2.fill(RECT_BG);
      g2.setColor(Color.WHITE);
      g2.fill(RECT_FG);
      
      for (LinkGraphic lg : deletedLinks)
         lg.draw(g2);
      
      for (BridgeIcon b : deletedBridges)
         b.draw(g2);
      
      if (bridges.size() < 1)
      {
         if (deletedBridges.size() < 1)
            drawStartMessage(g2);
         return;
      }
      
      for (LinkGraphic lg : links)
         lg.draw(g2);
      
      for (BridgeIcon b : bridges)
         b.draw(g2);
   }
   
   // ------[PUBLIC METHODS]------ //

   /** Attach a listener to the Model
       @param c the listener
       */
   
   public void attach(ChangeListener c)
   {
      if (c != null)
         listeners.add(c);
   }
   
   public boolean canDisableSelected()
   {
      if (selectedA == null && selectedB == null)
         return false;
      return ((selectedA != null && !selectedA.getBridge().DISABLED)
            || (selectedB != null && !selectedB.getBridge().DISABLED));
   }
   
   public boolean canEnableSelected()
   {
      if (selectedA == null && selectedB == null)
         return false;
      return ((selectedA != null && selectedA.getBridge().DISABLED)
            || (selectedB != null && selectedB.getBridge().DISABLED));
   }
   
   /** Clear all bridges and links. */
   
   public void clearData()
   {
      if (isRunning) // stop threads first
      {
         for (Thread t : threads)
            t.interrupt();
         
         threads.clear();
         isRunning = false;
      }
      
      for (LinkGraphic lg : links)
      {
         lg.destructor();
         deletedLinks.add(lg);
      }
      
      for (BridgeIcon b : bridges)
         deletedBridges.add(b);
      
      links.clear();
      bridges.clear();
      
      selectedA = null;
      selectedB = null;
      
      numBridgesMade = 0;
   }
   
   /** Deletes the selected bridge if only 1 is selected.
       Also removes all affected links. */
   
   public void deleteSelectedBridge()
   {
      if (getNumberOfSelectedBridges() != 1)
         return;
      
      deleteBridge(selectedA);
      triggerChangeEvent();
   }
   
   /** Disable selected bridges. */
   
   public void disableSelected()
   {
      if (selectedA != null)
         selectedA.getBridge().DISABLED = true;
      if (selectedB != null)
         selectedB.getBridge().DISABLED = true;
      triggerChangeEvent();
   }
   
   /** Enable selected bridges. */
   
   public void enableSelected()
   {
      if (selectedA != null)
         selectedA.getBridge().DISABLED = false;
      if (selectedB != null)
         selectedB.getBridge().DISABLED = false;
      triggerChangeEvent();
   }
   
   /** Returns first selected bridge. */
   public BridgeIcon getSelectedBridgeA() { return selectedA; }
   
   /** Returns the second selected bridge. */
   public BridgeIcon getSelectedBridgeB() { return selectedB; }
   
   /** Returns the number of bridges on the panel. */
   public int getNumberOfBridges() { return bridges.size(); }
   
   /** Returns how many bridges are selected (0, 1, or 2). */
   
   public int getNumberOfSelectedBridges()
   {
      if (selectedA == null && selectedB == null)
         return 0;
      if (selectedA == null || selectedB == null)
         return 1;
      return 2;
   }
   
   /** Returns whether a bridgeID is present. */
   
   public boolean isBridgeIDPresent(String id)
   {
      for (BridgeIcon b : bridges)
         if (id.equals(b.getID()))
            return true;
      return false;
   }
   
   /** Returns whether the simulation is currently running. */
   public boolean isRunning() { return isRunning; }
   
   /** Links the selected bridges. */
   public void linkSelected() { linkBridges(selectedA, selectedB); }
   
   /** Clears the current configuration & replaces it
       with a set of bridges and links stored in a file.
       */
   
   public void load()
   {
      if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION)
      {
         clearData();
         
         try // try to load data
         {
            File file = fileChooser.getSelectedFile();
            ObjectInputStream in = new ObjectInputStream(
                  new FileInputStream(file));
            
            try
            {
               while (true)
               {
                  String whatIsIt = (String)in.readObject();
                  
                  if (whatIsIt.compareTo(IS_BRIDGE) == 0)
                  {
                     BridgeIcon b = BridgeIcon.read(in);
                     addBridge(b);
                  }
                  else if (whatIsIt.compareTo(IS_LINK) == 0)
                  {
                     String idA = (String)in.readObject();
                     String idB = (String)in.readObject();
                     
                     BridgeIcon bridgeA = null;
                     BridgeIcon bridgeB = null;
                     
                     for (BridgeIcon b : bridges)
                        if (b.matches(idA))
                           bridgeA = b;
                        else if (b.matches(idB))
                           bridgeB = b;
                     
                     if (bridgeA == null || bridgeB == null)
                        System.out.println("File error: bad link!");
                     else linkBridges(bridgeA, bridgeB);
                  }
               }
            }
            catch(Exception e) { ; } // end of file or read error
            
            deletedBridges.clear();
            deletedLinks.clear();
            in.close();
         }
         catch (FileNotFoundException blackhole) { ; }
         catch (IOException needsTapioca) { ; }
      }
   }
   
   /** Starts the simulation running. */
   
   public void run()
   {
      if (isRunning)
         return;
      
      for (Thread t : threads)
         t.start();
      
      isRunning = true;
      triggerChangeEvent();
   }
   
   /** Saves the current configuration to a file. */
   
   public void save()
   {
      if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION)
      {
         try
         {
            File file = fileChooser.getSelectedFile();
            ObjectOutputStream out = new ObjectOutputStream(
                  new FileOutputStream(file));
            
            for (BridgeIcon b : bridges)
            {
               out.writeObject(IS_BRIDGE);
               b.write(out);
            }
            
            for (LinkGraphic lg : links)
            {
               // assumes IDs are unique:
               out.writeObject(IS_LINK);
               out.writeObject(lg.getBridgeIconA().getID());
               out.writeObject(lg.getBridgeIconB().getID());
            }
            
            out.close();
         }
         catch (IOException sadface) { ; }
      }
   }
   
   /** Returns whether the selected bridges are linked.
       @return whether the selected bridges are linked
       */
   
   public boolean selectedBridgesAreLinked()
   {
      return areLinked(selectedA, selectedB);
   }
   
   /** Unlinks selected bridges, if they are linked. */
   
   public void unlinkSelected()
   {
      removeLink(selectedA, selectedB);
   }
   
   // ------[PRIVATE HELPER METHODS]------ //
   
   /** Adds an existing bridge to the panel. */
   
   private void addBridge(BridgeIcon b)
   {
      Thread t = new Thread(b.getBridge());
      bridges.add(b);
      threads.add(t);
      
      if (isRunning)
         t.start();
   }
   
   /** Adds a new bridge to the panel. */
   
   private void addNewBridge(int x, int y)
   {
      String newID = String.format("B%1$02d", numBridgesMade++);
      
      // make sure it's unique!
      while (isBridgeIDPresent(newID))
         newID = String.format("B%1$02d", numBridgesMade++);
      
      addBridge(new BridgeIcon(new Bridge(newID), x, y));
   }
   
   /** Returns whether two bridges on the panel are already linked */
   
   private boolean areLinked(BridgeIcon a, BridgeIcon b)
   {
      if (a == null || b == null || a == b)
         return false;
      
      return a.getBridge().isLinkedDirectlyTo(b.getBridge());
   }
   
   /** Removes a bridge from the simulation.
       @param b the bridge to delete
       */
   
   private void deleteBridge(BridgeIcon b)
   {
      // ------:is b a selected bridge? unselect it:------
      
      if (b == selectedB) selectedB = null;
      if (b == selectedA) { selectedA = selectedB; selectedB = null; }
      
      // ------:first stop b's thread:------
      
      for (int i = 0; i < bridges.size(); i++)
      {
         if (bridges.get(i) == b)
         {
            threads.get(i).interrupt();
            threads.remove(i);
            break;
         }
      }
      
      // ------:remove attached links first:------
      
      Iterator<LinkGraphic> iter = links.iterator();
      
      while (iter.hasNext())
      {
         LinkGraphic lg = iter.next();
         
         if (lg.isLinkTo(b))
         {
            lg.destructor();
            iter.remove(); // byes! :(
            deletedLinks.add(lg);
         }
      }
      
      // ------:then remove bridge:------
      
      Iterator<BridgeIcon> iterBridge = bridges.iterator();
      
      while (iterBridge.hasNext())
         if (iterBridge.next() == b)
            iterBridge.remove();
      
      if (threads.size() < 1)
      {
         isRunning = false;
         numBridgesMade = 0; // reset! :D
      }
      
      deletedBridges.add(b);
   }
   
   /** Displays a start message to help the user get going. */
   
   private void drawStartMessage(Graphics2D g2)
   {
      String message = "Click this space to place bridges!";
      Font font = new Font("Times New Roman", Font.ITALIC, 24);
      
      g2.setFont(font);
      FontMetrics fm = g2.getFontMetrics();
      Rectangle2D strRect = fm.getStringBounds(message, g2);
      int strX = (int)(((PANEL_WIDTH-strRect.getWidth())/2) + 0.01);
      int strY = (int)(((PANEL_HEIGHT-strRect.getHeight())/2) + 0.01);
      
      g2.setColor(Color.BLUE);
      g2.drawString(message, strX, strY);
   }
   
   /** Links two bridges. */
   
   private void linkBridges(BridgeIcon a, BridgeIcon b)
   {
      if (a == null || b == null || a == b)
         return;
      
      if (areLinked(a, b))
         return; // no good, stop trying that :)
      
      links.add(new LinkGraphic(a, b, linkCost));
      triggerChangeEvent();
   }
   
   /** Process a mouse click at (x,y). */
   
   private void processClick(int x, int y)
   {
      for (BridgeIcon b : bridges) // check to see if bridge exists
      {
         if (b.contains(x, y))
         {
            toggleSelect(b);
            return;
         }
      }
      
      if ((x < BUFFER_X)|| (x > (PANEL_WIDTH-BUFFER_X))
             || (y < BUFFER_Y) || (y > (PANEL_HEIGHT-BUFFER_Y)))
         return; // too close to edge
      
      for (BridgeIcon b : bridges)
         if (b.bufferSpaceContains(x, y))
            return; // too close to existing bridges
      
      // yay, we can make a new bridge! :D
      addNewBridge(x, y);
   }
   
   /** Removes the first link found between two bridge icons. */
   
   private void removeLink(BridgeIcon a, BridgeIcon b)
   {
      if (a == null || b == null)
         return;
      
      Iterator<LinkGraphic> iter = links.iterator();
      
      while (iter.hasNext())
      {
         LinkGraphic lg = iter.next();
         
         if (lg.isLinkBetween(a,b)) // found it! :D
         {
            lg.destructor();
            iter.remove(); // snip! :)
            deletedLinks.add(lg);
            triggerChangeEvent();
            return;
         }
      }
   }
   
   /** Toggles select on or off for a bridge. */
   
   private void toggleSelect(BridgeIcon b)
   {
      if (b == selectedA) // B already selected (1st)
      {
         b.setSelected(false);
         selectedA = selectedB;
         selectedB = null;
         return;
      }
      
      if (b == selectedB) // B already selected (2nd)
      {
         b.setSelected(false);
         selectedB = null;
         return;
      }
      
      if (selectedA == null) // no bridges selected
      {
         selectedA = b;
         b.setSelected(true);
         return;
      }
      
      if (selectedB == null) // only one bridge selected
      {
         selectedB = b;
         b.setSelected(true);
         return;
      }
      
      // two already selected? bump list down
      selectedA.setSelected(false);
      selectedA = selectedB;
      selectedB = b;
      b.setSelected(true);
   }
   
   /** Call this to trigger a change event on bridge data. */
   
   private void triggerChangeEvent()
   {
      for (ChangeListener l : listeners)
         l.stateChanged(new ChangeEvent(this));
   }
}
